/**
 * Checks if an element has a class name
 * @param {String} The class name
 * @return {Boolean}
 */
Element.prototype.hasClassName = document.documentElement.classList ? function(className) {
    return this.classList.contains(className);
} : function(className) {
    var elementClassName = this.className;

    if (elementClassName.length === 0) {
        return false;
    }
    if (elementClassName === className) {
        return true;
    }

    var regexp = new RegExp('(^|\\s+)' + className + '(\\s+|$)');
    return regexp.test(elementClassName);
};
Element.prototype.addClassName = document.documentElement.classList ? function(className) {
    return this.classList.add(className);
} : function(className) {
    var elementClassName = this.className;

    if (elementClassName.length === 0) {
        return false;
    }
    if (elementClassName === className) {
        return true;
    }

    var regexp = new RegExp('(^|\\s+)' + className + '(\\s+|$)');
    return regexp.test(elementClassName);
};
Element.prototype.removeClassName = document.documentElement.classList ? function(className) {
    return this.classList.remove(className);
} : function(className) {
    var elementClassName = this.className;

    if (elementClassName.length === 0) {
        return false;
    }
    if (elementClassName === className) {
        return true;
    }

    var regexp = new RegExp('(^|\\s+)' + className + '(\\s+|$)');
    return regexp.test(elementClassName);
};

// these need special handling
window.dataset = {};
document.dataset = {};
document.documentElement.dataset = {};

/**
 * Unique id generator
 */
GUID = {};

/**
 * Returns a new unique id
 * @return {String}
 */
GUID.guid = (function() {
    function s4() {
        return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
    }

    return function() {
        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
    };
})();

/**
 * Returns a GUID for element
 * @param {Element} The element
 * @return {String}
 */
GUID.getGUIDForElement = function(element) {
    if (element === document || element === document.documentElement) {
        return '!!document';
    }
    if (element === window) {
        return '!!window';
    }

    if (!element.dataset['guid']) {
        element.dataset['guid'] = element.id || GUID.guid();
    }

    element.dataset

    return element.dataset['guid'];
};

/**
 * Touch events layer
 */
Touch = {};

Touch.android = true;

/**
 * Whether this is a touch-capable browser or not
 */
Touch.touch = false;

/**
 * Whether the address bar is hidden
 */
Touch.addressBarHidden = false;

/**
 * 'touchstart' client X coordinate
 */
Touch.startClientX = 0;

/**
 * 'touchstart' client Y coordinate
 */
Touch.startClientY = 0;

/**
 * 'touchstart' screen X coordinate
 */
Touch.startScreenX = 0;

/**
 * 'touchstart' screen Y coordinate
 */
Touch.startScreenY = 0;

/**
 * Moved
 */
Touch.moved = false;

/**
 * The element the action started on
 */
Touch.target = null;

/**
 * Previous target
 */
Touch.prevTarget = null;

/**
 * Event callbacks
 */
Touch.boundEventCallbacks = {};

/**
 * Events that we handle
 */
Touch.handledEvents = {
    'touchstart'  : true,
    'touchmove'   : true,
    'touchend'    : true,
    'focus'       : true,
    'blur'        : true,
    'change'      : true,
    'click'       : true
};

/**
 * Initializer
 */
Touch.initialize = function() {
    if (!('ontouchstart' in window)) {
        // not touch - do nothing
        return;
    }

    this.touch = true;

    // ugly hack - defer window.alert so it doesn't backfire [touchStart event]
  /*  window.oldAlert = window.alert;
    window.alert = function(message) {
        setTimeout(function() {
            window.oldAlert(message);
        }, 0);
    };
*/
    var that = this;

    // subscribe for touchStart, touchMove, touchCancel & touchEnd events
    window.addEventListener('touchstart', function(event) {
        that.eventStart(event.target, event);
    });
    window.addEventListener('touchmove', function(event) {
        that.eventMove(event.target, event);
    });
    window.addEventListener('touchend', function(event) {
        that.eventEnd(event.target, event);
    });
    window.addEventListener('touchcancel', function(event) {
        that.eventCancel(event.target, event);
    });
};

/**
 * Handles touchStart event
 * @param {Element} The target element
 * @param {Event} The event object
 */
Touch.eventStart = function(target, event) {
    this.startClientX = event.touches[0].clientX;
    this.startClientY = event.touches[0].clientY;
    this.startScreenX = event.touches[0].screenX;
    this.startScreenY = event.touches[0].screenY;
    this.moved = false;

    if (this.target != target) {
        this.prevTarget = this.target;

        if (target.tagName == 'LABEL') {
            var associatedElement = document.getElementById(target.htmlFor);
            if (associatedElement) {
                target = associatedElement;
            }
        }
        this.target = target;
        this.fire(this.target, 'touchstart', event);

        if (this.android && this.isActivatable(target)) {
            this.pseudoActivate(target);
        }
        if (!this.isScrollable(target) || !this.isInput(target)) {
            //event.preventDefault();
            event.stopPropagation();
        }
    }
};

/**
 * Handles touchMove event
 * @param {Element} The target element
 * @param {Event} The event object
 */
Touch.eventMove = function(target, event) {
    var
        clientX = event.touches[0].clientX,
        clientY = event.touches[0].clientY,
        screenX = event.touches[0].screenX,
        screenY = event.touches[0].screenY;

    if (Math.abs(clientX - this.startClientX) > 10 || Math.abs(clientY - this.startClientY) > 10) {
        this.moved = true;

        target = document.elementFromPoint(clientX, clientY);
        if (!target || (target.nodeType == 3 && !(target = target.parentNode))) {
            return;
        }
        this.fire(target, 'touchmove', event);
    }

    if (this.isScrollable(target)) {
        // here comes trouble..
        var container = target;
        while (container && container != document.body && !container.hasClassName('content') && !container.hasClassName('scrollable')) {
            container = container.parentNode;
        }

        if (!container) {
            return;
        }

        var
            top = container.scrollTop,
            bottom = container.scrollHeight - container.scrollTop - container.offsetHeight,
            dirX = Math.abs(screenX - this.startScreenX),
            dirY = Math.abs(screenY - this.startScreenY);

        if (top == 0 && screenY > this.startScreenY) {
            //event.preventDefault();
            if (dirX > dirY) {
                container.scrollTop = 1;
            } else {
                event.preventDefault();
            }
        } else if (bottom == 0 && screenY < this.startScreenY) {
            //event.preventDefault();
            if (dirX > dirY) {
                container.scrollTop = container.scrollHeight - container.offsetHeight - 1;
            } else {
                event.preventDefault();
            }
        }
    } else {
        event.preventDefault();
        event.stopPropagation();
    }
};

/**
 * Handles touchEnd event
 * @param {Element} The target element
 * @param {Event} The event object
 */
Touch.eventEnd = function(target, event) {
    this.fire(this.target, "touchend", event);
    if (this.moved) {
        target = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
        if (!target || (target.nodeType == 3 && !(target = target.parentNode))) {
            return;
        }
        if (target.tagName == 'LABEL') {
            var associatedElement = document.getElementById(target.htmlFor);
            if (associatedElement) {
                target = associatedElement;
            }
        }
        if (target != this.target || this.android || this.isScrollable(target)) {

            if (this.android) {
                this.pseudoDeactivate(this.target);
            }

            this.target = null;
            return;
        }
    }
    if (this.target != this.prevTarget) {
        if (this.isInput(this.prevTarget)) {
            this.prevTarget.blur();
        }
        if (this.isInput(this.target)) {
            //this.target.focus();
            return;
        }
        if (this.target != document.activeElement && document.activeElement.blur) {
            document.activeElement.blur();
        }
    }
    if (this.isClickable(this.target)) {
        // if (document.activeElement && document.activeElement.blur) {
        //   document.activeElement.blur();
        // }

        if (this.android) {
            this.pseudoDeactivate(this.target);
        }

        this.fire(this.target, 'click', event);
        this.target = null;
        event.preventDefault();
        event.stopPropagation();

        // var that = this;
        // setTimeout(function() {
        //   that.target = null;
        // }, 0)
    }
};

/**
 * Handles touchCancel event
 * @param {Element} The target element
 * @param {Event} The event object
 */
Touch.eventCancel = function(target, event) {
    if (this.android) {
        this.pseudoDeactivate(target);
    }
    this.target = null;
};

/**
 * Checks if element is an input
 * @param {Element} The element
 * @return {Boolean}
 */
Touch.isInput = function(element) {
    if (element && (element.tagName == 'TEXTAREA' || element.tagName == 'SELECT'  || (element.tagName == 'INPUT' && element.type != 'checkbox' && element.type != 'radio' && element.type != 'button') || (element.contentEditable == 'true'))) {
        return true;
    }

    return false;
};

/**
 * Checks if element is clickable
 * @param {Element} The element
 * @return {Boolean}
 */
Touch.isClickable = function(element) {
    var docBody = document.body;

    do {
        if(!element) return;
        if (((element.hasClassName('action') || element.dataset['action']) && element.tagName != 'FORM') || element.hasOwnProperty("onclick") || element.tagName == 'BUTTON' || (element.tagName == 'INPUT' && element.type == 'button')) {
            return true;
        }
        element = element.parentNode;
    } while (element && element != docBody && element != document);

    return false;
};

/**
 * Checks if element is scrollable
 * @param {Element} The element
 * @return {Boolean}
 */
Touch.isScrollable = function(element) {
    var docBody = document.body;
    if(this.android) return false;
    do {
        if(!element) return;
        if (element.hasClassName('content') || element.hasClassName('scrollable')) {
            return true;
        }
        element = element.parentNode;
    } while (element && element != docBody && element != document);

    return false;
};

/**
 * Checks if element needs .pseudo-active
 * @param {Element} The element
 * @return {Boolean}
 */
Touch.isActivatable = function(element) {
    var docBody = document.body;

    do {
        if(!element) return;
        if (element.hasClassName('activatable')) {
            return true;
        }
        element = element.parentNode;
    } while (element && element != docBody && element != document && element != document);

    return false;
};

/**
 * Adds the .pseudo-active class
 * @param {Element} The element
 */
Touch.pseudoActivate = function(element) {
    var docBody = document.body;

    do {
        if(!element) return;
        if (element.hasClassName('activatable')) {
            element.addClassName('pseudo-active');
        }
        element = element.parentNode;
    } while (element && element != docBody && element != document);
};

/**
 * Removes the .pseudo-active class
 * @param {Element} The element
 */
Touch.pseudoDeactivate = function(element) {
    var docBody = document.body;

    do {
        if(!element) return;
        if (element.hasClassName('pseudo-active')) {
            element.removeClassName('pseudo-active');
        }
        element = element.parentNode;
    } while (element && element != docBody && element != document);
};

/**
 * Hides the address bar
 */
Touch.hideAddressBar = function() {
    if (this.touch && !Base.Browser.PhoneGap && Base.Browser.iPhone && !Base.Browser.Fullscreen && !this.addressBarHidden) {
        var hideAddressBar = function() {
            switch (window.orientation) {
                case -90:
                case 90:
                    document.body.style.height = window.screen.width + 'px';
                    $('wrapper').style.height = (window.screen.width - 60) + 'px';
                    break;
                default:
                    document.body.style.height = window.screen.height + 'px';
                    $('wrapper').style.height = (window.screen.height - 60) + 'px';
                    break;
            }

            window.scrollTo(0, 1);
        };

        this.addressBarHidden = true;

        window.addEventListener('orientationchange', hideAddressBar);
        setTimeout(hideAddressBar, 0);
    }
};

/**
 * "Fires" an event for an element
 * @param {Element} Element
 * @param {String} Event name
 * @param {Event} DOM event
 */
Touch.fire = function(element, eventName, domEvent) {
    var guid = GUID.getGUIDForElement(element);

    if (!guid) {
        return false;
    }

    if (this.handledEvents[eventName]) {
        // rewrite the stopPropagation() method so we handle it ourselves
        domEvent.nativeStopPropagation = domEvent.stopPropagation;
        domEvent.stopPropagation = function() {
            this.propagationStopped = true;
        };

        // execute events from the chain
        if (this.boundEventCallbacks[guid] && this.boundEventCallbacks[guid][eventName]) {
            for (var index = 0, length = this.boundEventCallbacks[guid][eventName].length; index < length; ++index) {
                domEvent.propagationStopped = false;

                try {
                    this.boundEventCallbacks[guid][eventName][index](domEvent);
                } catch (e) {
                }
            }
        }

        // propagate
        if (!domEvent.propagationStopped && element.parentNode) {
            this.fire(element.parentNode, eventName, domEvent);
        }
    } else {
        // TODO: find out what to do here...
    }
};

/**
 * Adds an event responder to element's event chain
 * @param {Element} The element
 * @param {String} Event name
 * @param {Function} Callback
 * @param {Boolean} Bubble bobble
 */
Touch.observe = function(element, eventName, callback, bubbleBobble) {
    if (typeof element == 'string') {
        element = document.getElementById(element);
    }
    if (this.handledEvents[eventName]) {
        var guid = GUID.getGUIDForElement(element);

        if (!this.boundEventCallbacks[guid]) {
            this.boundEventCallbacks[guid] = {};
        }
        if (!this.boundEventCallbacks[guid][eventName]) {
            this.boundEventCallbacks[guid][eventName] = [];
        }

        this.boundEventCallbacks[guid][eventName][bubbleBobble ? 'unshift' : 'push'](callback);
    } else {
        element.addEventListener(eventName, callback, bubbleBobble || false);
    }
};

/**
 * Removes an event responder from element's event chain
 * @param {Element} The element
 * @param {String} Event name
 * @param {Function} Callback
 */
Touch.stopObserving = function(element, eventName, callback) {
    if (typeof element == 'string') {
        element = document.getElementById(element);
    }

    if (this.handledEvents[eventName]) {
        var guid = element.dataset['guid'];

        if (this.boundEventCallbacks[guid] && this.boundEventCallbacks[guid][eventName]) {
            if (callback) {
                this.boundEventCallbacks[guid][eventName].remove(callback);
            } else {
                this.boundEventCallbacks[guid][eventName] = [];
            }
        }
    } else {
        element.removeEventListener(eventName, callback);
    }
};

// initialize ourselves even before we load :d
Touch.initialize();